> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a Frontend for Sei EVM: A Comprehensive Guide

> Comprehensive guide on Building a Frontend for Sei EVM. Learn key concepts, commands, and best practices.

To build the frontend of a dApp on Sei EVM, you connect to wallets and interact with the blockchain through RPC endpoints. You also sign and broadcast transactions. This tutorial shows how to build a simple ERC20 token interface with three popular libraries:

1. [**ethers.js**](https://docs.ethers.org/v6/): A complete and compact library for interacting with
   EVM blockchains. It is known for its simplicity and extensive functionality.
2. [**viem**](https://viem.sh/docs/getting-started): A lightweight and modular TypeScript interface for Ethereum.
3. [**wagmi**](https://wagmi.sh/): A React hooks library, built on top of viem, that simplifies wallet
   connection and interaction. It has hooks that modern frontend libraries and frameworks can use
   to interact with Ethereum wallets and contracts.

The tutorial implements the same functionality with each library. You can then compare their approaches and choose the one that best fits your development style.

<Tip>
  **Deploy to Sei Testnet first**

  It is <strong>highly recommended</strong> that you deploy to <em>Sei Testnet</em> first. Verify that everything works as expected before you commit to Sei Mainnet. This helps you catch bugs early, avoid unnecessary gas costs, and keep your users safe.
</Tip>

## When to use each library

### ethers.js

ethers.js is ideal for developers who want a complete, battle-tested library with a straightforward API design. It works well for both simple and complex dApps, especially when you do not use React or you need custom state management.

* **Pros**:
  * Complete and easy-to-use API
  * Well documented, with a large community
  * Works well with both TypeScript and JavaScript
  * All-in-one solution for wallet connection and contract interaction
* **Cons**:
  * Larger bundle size than viem
  * Not designed specifically for integration with React hooks

### viem

viem suits developers who want fine-grained control over their blockchain interactions and prefer a modular, lightweight approach. It is a good choice when bundle size matters and when you have specific requirements for how contract interactions should work.

* **Pros**:
  * Lightweight and modular
  * Excellent TypeScript support with better type safety
  * Lower-level API that gives more control
  * Smaller bundle size
* **Cons**:
  * Steeper learning curve
  * Requires more boilerplate code for some operations
  * Requires separate handling for wallet connection and contract interactions

### wagmi

wagmi is the best choice for React developers who build dApps and want to use React's state management capabilities. Its hooks hide much of the complexity of blockchain interactions. This makes it easy to build reactive UIs that respond to chain state.

* **Pros**:
  * React-specific hooks
  * Handles complex state management for you
  * Built on top of viem, so it shares the benefits of viem
  * Convenient caching and automatic refreshing of contract data
* **Cons**:
  * Only works with React
  * Adds another dependency layer
  * Opinionated about how you should manage data in your app

## Requirements

Before you start, make sure that you have:

* Node.js and npm installed
* A wallet from the [list of Sei wallets](/learn/wallets)

## Creating a React project

Create a new React project with Vite's TypeScript template:

```bash theme={null}
npm create vite@latest sei-token-interface -- --template react-ts
```

This command creates a new folder with a TypeScript React project. Open `sei-token-interface` in your preferred IDE.

<Info>This tutorial uses TypeScript. If you do not use TypeScript, you can adjust the code by removing the types.</Info>

## Project structure

For clarity, each library implementation has its own component. First, set up the project structure:

```bash theme={null}
cd sei-token-interface
mkdir src/components
touch src/components/EthersInterface.tsx
touch src/components/ViemInterface.tsx
touch src/components/WagmiInterface.tsx
mkdir src/shared
touch src/shared/constants.ts
touch src/wagmi.ts
```

## Defining the ERC20 contract details

<Info>First, deploy your ERC20 token contract. For example, use [Hardhat](/evm/evm-hardhat) or [Foundry](/evm/evm-foundry), or generate a contract with the [contract wizard](/evm/evm-wizard) (see also [deploy and verify](/evm/evm-parity/examples/deploy-verify)). Then replace `TOKEN_CONTRACT_ADDRESS` in the constants file with the address of your deployed contract. Also update the RPC URL in the Sei chain configuration. For a list of existing ERC20 contracts on Sei Mainnet, see [Sei Assets](https://seiscan.io/tokens).</Info>

Create a shared constants file for the project:

```tsx title="src/shared/constants.ts" theme={null}
// Constants used across different implementations
export const ERC20_ABI = [
  {
    inputs: [],
    name: 'name',
    outputs: [
      {
        internalType: 'string',
        name: '',
        type: 'string'
      }
    ],
    stateMutability: 'view',
    type: 'function'
  },
  {
    inputs: [],
    name: 'symbol',
    outputs: [
      {
        internalType: 'string',
        name: '',
        type: 'string'
      }
    ],
    stateMutability: 'view',
    type: 'function'
  },
  {
    inputs: [],
    name: 'decimals',
    outputs: [
      {
        internalType: 'uint8',
        name: '',
        type: 'uint8'
      }
    ],
    stateMutability: 'view',
    type: 'function'
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'account',
        type: 'address'
      }
    ],
    name: 'balanceOf',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256'
      }
    ],
    stateMutability: 'view',
    type: 'function'
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'to',
        type: 'address'
      },
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256'
      }
    ],
    name: 'transfer',
    outputs: [
      {
        internalType: 'bool',
        name: '',
        type: 'bool'
      }
    ],
    stateMutability: 'nonpayable',
    type: 'function'
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'from',
        type: 'address'
      },
      {
        indexed: true,
        internalType: 'address',
        name: 'to',
        type: 'address'
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'value',
        type: 'uint256'
      }
    ],
    name: 'Transfer',
    type: 'event'
  }
];

// TODO: Deploy an ERC-20 (see /evm/evm-hardhat or /evm/evm-foundry) and paste its address here
export const TOKEN_CONTRACT_ADDRESS = '0xYourTokenContractAddress';
```

## Option 1: ethers.js implementation

Install ethers.js first:

```bash theme={null}
npm install ethers
```

Start with the ethers.js implementation. The component does these things:

* Checks for any EVM-compatible wallet extension.
* Connects to Sei Mainnet through the connected wallet, with the ethers.js
  `BrowserProvider`.
* Creates an ethers.js contract instance with the signer from the wallet.
  It stores the instance in the contract state for later use.

```tsx title="src/components/EthersInterface.tsx" theme={null}
import { useState, useEffect } from 'react';
import { BrowserProvider, Contract, formatEther, parseEther } from 'ethers';
import { ERC20_ABI, TOKEN_CONTRACT_ADDRESS } from '../shared/constants';

export function EthersInterface() {
  const [balance, setBalance] = useState<string>();
  const [contract, setContract] = useState<Contract>();
  const [recipientAddress, setRecipientAddress] = useState('');
  const [amount, setAmount] = useState('');
  const [isTransferring, setIsTransferring] = useState(false);
  const [tokenInfo, setTokenInfo] = useState<{ name: string; symbol: string }>();
  const [address, setAddress] = useState<string>();

  // Sei EVM network configuration
  const SEI_NETWORK_PARAMS = {
    chainId: '0x531', // 1329 in hexadecimal
    chainName: 'Sei Network',
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    rpcUrls: ['https://evm-rpc.sei-apis.com'],
    blockExplorerUrls: ['https://seiscan.io']
  };

  const fetchBalance = async () => {
    if (!contract || !address) return;
    try {
      const balance = await contract.balanceOf(address);
      setBalance(formatEther(balance));
    } catch (error) {
      console.error('Failed to fetch balance:', error);
    }
  };

  const fetchTokenInfo = async () => {
    if (!contract) return;
    try {
      const name = await contract.name();
      const symbol = await contract.symbol();
      setTokenInfo({ name, symbol });
    } catch (error) {
      console.error('Failed to fetch token info:', error);
    }
  };

  useEffect(() => {
    if (contract) {
      fetchTokenInfo();
      fetchBalance();
    }
  }, [contract, address]);

  const connectWallet = async () => {
    if (window.ethereum) {
      try {
        const provider = new BrowserProvider(window.ethereum);
        // Attempt to switch to the Sei network
        try {
          await provider.send('wallet_switchEthereumChain', [{ chainId: SEI_NETWORK_PARAMS.chainId }]);
        } catch (switchError: any) {
          // Error code 4902 indicates the chain is not added in MetaMask
          if (switchError.code === 4902) {
            await provider.send('wallet_addEthereumChain', [SEI_NETWORK_PARAMS]);
          } else {
            throw switchError;
          }
        }
        // Request account access
        await provider.send('eth_requestAccounts', []);
        const signer = await provider.getSigner();
        const userAddress = await signer.getAddress();
        setAddress(userAddress);
        const tokenContract = new Contract(TOKEN_CONTRACT_ADDRESS, ERC20_ABI, signer);
        setContract(tokenContract);
      } catch (error) {
        console.error('Failed to connect wallet:', error);
        alert('Failed to connect wallet. See console for details.');
      }
    } else {
      alert('No EVM compatible wallet installed');
    }
  };

  const transferTokens = async () => {
    if (!contract || !recipientAddress || !amount) return;
    try {
      setIsTransferring(true);
      const tx = await contract.transfer(recipientAddress, parseEther(amount));
      console.log('Transaction sent:', tx.hash);
      const receipt = await tx.wait();
      console.log('Transaction confirmed:', receipt);
      await fetchBalance();
      setRecipientAddress('');
      setAmount('');
    } catch (error) {
      console.error('Transfer failed:', error);
      alert('Transfer failed. Check console for details.');
    } finally {
      setIsTransferring(false);
    }
  };

  return (
    <div className="card">
      <h2>Ethers.js v6 Implementation</h2>
      {contract ? (
        <div>
          <h3>
            {tokenInfo?.name} ({tokenInfo?.symbol})
          </h3>
          <p>
            Connected Address: {address?.slice(0, 6)}...{address?.slice(-4)}
          </p>
          <p>
            Balance: {balance} {tokenInfo?.symbol}
          </p>
          <div style={{ marginTop: '20px' }}>
            <input type="text" placeholder="Recipient Address" value={recipientAddress} onChange={(e) => setRecipientAddress(e.target.value)} style={{ marginBottom: '10px', width: '300px' }} />
            <br />
            <input type="number" placeholder="Amount" value={amount} onChange={(e) => setAmount(e.target.value)} style={{ marginBottom: '10px', width: '300px' }} />
            <br />
            <button disabled={isTransferring} onClick={transferTokens}>
              {isTransferring ? 'Transferring...' : 'Transfer Tokens'}
            </button>
          </div>
        </div>
      ) : (
        <button onClick={connectWallet}>Connect with Ethers.js v6</button>
      )}
    </div>
  );
}
```

## Option 2: viem implementation

Install viem first:

```bash theme={null}
npm install viem
```

Now implement the viem interface:

```tsx title="src/components/ViemInterface.tsx" theme={null}
import { useState, useEffect } from 'react';
import { createWalletClient, custom, parseEther, formatEther } from 'viem';
import { createPublicClient, http } from 'viem';
import { sei } from 'viem/chains';
import { ERC20_ABI, TOKEN_CONTRACT_ADDRESS } from '../shared/constants';

export function ViemInterface() {
  const [balance, setBalance] = useState<string>();
  const [address, setAddress] = useState<string>();
  const [walletClient, setWalletClient] = useState<any>(null);
  const [publicClient, setPublicClient] = useState<any>(null);
  const [recipientAddress, setRecipientAddress] = useState('');
  const [amount, setAmount] = useState('');
  const [isTransferring, setIsTransferring] = useState(false);
  const [tokenInfo, setTokenInfo] = useState<{ name: string; symbol: string }>();

  useEffect(() => {
    // Initialize the public client
    const newPublicClient = createPublicClient({
      chain: sei,
      transport: http()
    });
    setPublicClient(newPublicClient);
  }, []);

  useEffect(() => {
    if (walletClient && publicClient && address) {
      fetchTokenInfo();
      fetchBalance();
    }
  }, [walletClient, publicClient, address]);

  const fetchBalance = async () => {
    if (!publicClient || !address) return;

    try {
      const balance = await publicClient.readContract({
        address: TOKEN_CONTRACT_ADDRESS as `0x${string}`,
        abi: ERC20_ABI,
        functionName: 'balanceOf',
        args: [address]
      });

      setBalance(formatEther(balance as bigint));
    } catch (error) {
      console.error('Error fetching balance:', error);
    }
  };

  const fetchTokenInfo = async () => {
    if (!publicClient) return;

    try {
      const [name, symbol] = await Promise.all([
        publicClient.readContract({
          address: TOKEN_CONTRACT_ADDRESS,
          abi: ERC20_ABI,
          functionName: 'name'
        }),
        publicClient.readContract({
          address: TOKEN_CONTRACT_ADDRESS,
          abi: ERC20_ABI,
          functionName: 'symbol'
        })
      ]);

      setTokenInfo({ name: name as string, symbol: symbol as string });
    } catch (error) {
      console.error('Error fetching token info:', error);
    }
  };
  const connectWallet = async () => {
    if (!window.ethereum) {
      alert('No EVM compatible wallet installed');
      return;
    }

    try {
      const [userAddress] = await window.ethereum.request({
        method: 'eth_requestAccounts'
      });

      setAddress(userAddress);

      const newWalletClient = createWalletClient({
        chain: sei,
        transport: custom(window.ethereum)
      });

      setWalletClient(newWalletClient);
    } catch (error) {
      console.error('Failed to connect wallet:', error);
      alert('Failed to connect wallet. See console for details.');
    }
  };

  const transferTokens = async () => {
    if (!walletClient || !address || !recipientAddress || !amount) return;

    try {
      setIsTransferring(true);

      // Prepare the contract call parameters
      const abi = ERC20_ABI;
      const functionName = 'transfer';
      const args = [recipientAddress, parseEther(amount)];

      // Execute the transaction
      const hash = await walletClient.writeContract({
        address: TOKEN_CONTRACT_ADDRESS as `0x${string}`,
        abi,
        functionName,
        args
      });

      console.log('Transaction sent:', hash);

      // Wait for the transaction to be mined
      const receipt = await publicClient.waitForTransactionReceipt({ hash });
      console.log('Transaction confirmed:', receipt);

      // Refresh the balance
      await fetchBalance();

      // Reset form
      setRecipientAddress('');
      setAmount('');
    } catch (error) {
      console.error('Transfer failed:', error);
      alert('Transfer failed. Check console for details.');
    } finally {
      setIsTransferring(false);
    }
  };

  return (
    <div className="card">
      <h2>Viem Implementation</h2>
      {address ? (
        <div>
          <h3>
            {tokenInfo?.name} ({tokenInfo?.symbol})
          </h3>
          <p>
            Connected Address: {address.slice(0, 6)}...{address.slice(-4)}
          </p>
          <p>
            Balance: {balance} {tokenInfo?.symbol}
          </p>
          <div style={{ marginTop: '20px' }}>
            <input type="text" placeholder="Recipient Address" value={recipientAddress} onChange={(e) => setRecipientAddress(e.target.value)} style={{ marginBottom: '10px', width: '300px' }} />
            <br />
            <input type="number" placeholder="Amount" value={amount} onChange={(e) => setAmount(e.target.value)} style={{ marginBottom: '10px', width: '300px' }} />
            <br />
            <button disabled={isTransferring} onClick={transferTokens}>
              {isTransferring ? 'Transferring...' : 'Transfer Tokens'}
            </button>
          </div>
        </div>
      ) : (
        <button onClick={connectWallet}>Connect with Viem</button>
      )}
    </div>
  );
}
```

## Option 3: wagmi implementation

Install wagmi and configure it:

```bash theme={null}
npm install wagmi viem @tanstack/react-query
```

First, create a wagmi configuration file:

```tsx title="src/wagmi.ts" theme={null}
import { http, createConfig } from 'wagmi';
import { sei, seiTestnet } from 'wagmi/chains';
import { injected } from 'wagmi/connectors';

export const config = createConfig({
  chains: [sei, seiTestnet],
  connectors: [injected()],
  transports: {
    [sei.id]: http(),
    [seiTestnet.id]: http()
  }
});
```

Now implement the wagmi interface:

```tsx title="src/components/WagmiInterface.tsx" theme={null}
import { useState, useEffect } from 'react';
import { useAccount, useConnect, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { injected } from 'wagmi/connectors';
import { parseEther, formatEther } from 'viem';
import { ERC20_ABI, TOKEN_CONTRACT_ADDRESS } from '../shared/constants';

export function WagmiInterface() {
  const [recipientAddress, setRecipientAddress] = useState('');
  const [amount, setAmount] = useState('');

  // Wagmi hooks
  const { address, isConnected } = useAccount();
  const { connect } = useConnect();

  // For debugging
  console.log('Connection status:', { address, isConnected });

  // Read from contract
  const { data: name } = useReadContract({
    address: TOKEN_CONTRACT_ADDRESS as `0x${string}`,
    abi: ERC20_ABI,
    functionName: 'name'
  });

  const { data: symbol } = useReadContract({
    address: TOKEN_CONTRACT_ADDRESS as `0x${string}`,
    abi: ERC20_ABI,
    functionName: 'symbol'
  });

  const { data: balance, refetch: refetchBalance } = useReadContract({
    address: TOKEN_CONTRACT_ADDRESS as `0x${string}`,
    abi: ERC20_ABI,
    functionName: 'balanceOf',
    args: address ? [address] : undefined,
    query: {
      enabled: !!address
    }
  });

  // Write to contract
  const { writeContract, data: hash, isPending: isTransferring, error } = useWriteContract();

  // Wait for transaction
  const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
    hash
  });

  // Connect wallet
  const connectWallet = async () => {
    try {
      connect({ connector: injected() });
    } catch (err) {
      console.error('Failed to connect:', err);
    }
  };

  // Transfer tokens
  const transferTokens = async () => {
    if (!recipientAddress || !amount) return;
    try {
      writeContract({
        address: TOKEN_CONTRACT_ADDRESS as `0x${string}`,
        abi: ERC20_ABI,
        functionName: 'transfer',
        args: [recipientAddress, parseEther(amount)]
      });
    } catch (err) {
      console.error('Transfer failed:', err);
    }
  };

  // Handle successful transfer
  useEffect(() => {
    if (isConfirmed) {
      refetchBalance();
      setRecipientAddress('');
      setAmount('');
    }
  }, [isConfirmed, refetchBalance]);

  return (
    <div className="card">
      <h2>Wagmi Implementation</h2>
      {isConnected ? (
        <div>
          <h3>
            {(name as string) || 'Loading...'} ({(symbol as string) || '...'})
          </h3>
          <p>
            Connected Address: {address?.slice(0, 6)}...{address?.slice(-4)}
          </p>
          <p>
            Balance: {balance ? formatEther(balance as bigint) : '0'} {(symbol as string) || ''}
          </p>
          <div style={{ marginTop: '20px' }}>
            <input type="text" placeholder="Recipient Address" value={recipientAddress} onChange={(e) => setRecipientAddress(e.target.value)} style={{ marginBottom: '10px', width: '300px' }} />
            <br />
            <input type="number" placeholder="Amount" value={amount} onChange={(e) => setAmount(e.target.value)} style={{ marginBottom: '10px', width: '300px' }} />
            <br />
            <button disabled={isTransferring || isConfirming} onClick={transferTokens}>
              {isTransferring ? 'Preparing Transaction...' : isConfirming ? 'Confirming Transaction...' : 'Transfer Tokens'}
            </button>
            {error && <p style={{ color: 'red' }}>Error: {(error as Error).message}</p>}
          </div>
        </div>
      ) : (
        <button onClick={connectWallet}>Connect with Wagmi</button>
      )}
    </div>
  );
}
```

## Updating the main app to display all implementations

Now update your `App.tsx` to include all three interface options:

```tsx title="src/App.tsx" theme={null}
import { useState } from 'react';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { EthersInterface } from './components/EthersInterface';
import { ViemInterface } from './components/ViemInterface';
import { WagmiInterface } from './components/WagmiInterface';
import { config } from './wagmi';

function App() {
  const [selectedLib, setSelectedLib] = useState<string | null>(null);

  const queryClient = new QueryClient();

  return (
    <div className="app-container">
      <h1>Sei ERC20 Token Interface</h1>
      <p>Choose a library implementation:</p>

      <div className="button-group">
        <button onClick={() => setSelectedLib('ethers')} className={selectedLib === 'ethers' ? 'active' : ''}>
          Ethers.js
        </button>
        <button onClick={() => setSelectedLib('viem')} className={selectedLib === 'viem' ? 'active' : ''}>
          Viem
        </button>
        <button onClick={() => setSelectedLib('wagmi')} className={selectedLib === 'wagmi' ? 'active' : ''}>
          Wagmi (React Hooks)
        </button>
      </div>

      <div className="interface-container">
        {selectedLib === 'ethers' && <EthersInterface />}
        {selectedLib === 'viem' && <ViemInterface />}
        {selectedLib === 'wagmi' && (
          <WagmiProvider config={config}>
            <QueryClientProvider client={queryClient}>
              <WagmiInterface />
            </QueryClientProvider>
          </WagmiProvider>
        )}
        {!selectedLib && (
          <div className="placeholder">
            <p>Select a library to see its implementation</p>
          </div>
        )}
      </div>
    </div>
  );
}

export default App;
```

## Polyfills for browser environment

When you develop frontend applications for the blockchain, you might need polyfills for Node.js-specific features, such as `Buffer`. Add these polyfills to your project:

```tsx title="src/main.tsx" theme={null}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { Buffer } from 'buffer';

// Polyfill self for browser and global for Node.js
const globalObject = typeof self !== 'undefined' ? self : global;

Object.assign(globalObject, {
  Buffer: Buffer
});

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

## Running the application

To start your app, run this command:

```bash theme={null}
npm run dev
```

This command starts a local development server at `http://localhost:5173`. You can switch between the library implementations to see how each one works.

To build your application, run this command:

```bash theme={null}
npm run build
```

Then preview the production build locally:

```bash theme={null}
npm run preview
```

This command serves the built app at `http://localhost:4173`, so you can verify it before you deploy. To go live, deploy the contents of the `dist` directory to any static hosting provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.