Skip to content
Logo

Build a frontend

This guide will walk you through the process of building a minimal frontend for your counter smart contract.

Prerequisites

Getting started

Set up your Counter contract ABI

After deploying your Counter contract from the Foundry quickstart, create an ABI file for your frontend:

// src/abi/Counter.ts
export const counterABI = [
  {
    name: 'number',
    type: 'function',
    inputs: [],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view'
  },
  {
    name: 'increment',
    type: 'function',
    inputs: [],
    outputs: [],
    stateMutability: 'nonpayable'
  },
  {
    name: 'setNumber',
    type: 'function',
    inputs: [{ name: 'newNumber', type: 'uint256' }],
    outputs: [],
    stateMutability: 'nonpayable'
  }
] as const
 
// Replace with your deployed contract address
export const COUNTER_ADDRESS = '0x...' as const

Configure wagmi

Then update your wagmi configuration to use environment variables:

// src/wagmi.ts
import { createConfig, http } from 'wagmi'
import { metaMask } from 'wagmi/connectors'
 
export const eden = {
  id: 714,
  name: 'Eden',
  nativeCurrency: { name: 'TIA', symbol: 'TIA', decimals: 18 },
  rpcUrls: {
    default: {
      http: ['https://rpc.eden.gateway.fm/'],
      webSocket: ['wss://rpc.eden.gateway.fm/ws']
    }
  },
  blockExplorers: {
    default: { name: 'Blockscout', url: 'https://eden.blockscout.com/' }
  }
} as const
 
export const config = createConfig({
  chains: [eden],
  connectors: [metaMask()],
  transports: { [eden.id]: http('https://rpc.eden.gateway.fm/') }
})

Create the Counter component

// src/components/Counter.tsx
import { useState } from 'react'
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { counterABI, COUNTER_ADDRESS } from '../abi/Counter'
 
export function Counter() {
  const [newNumber, setNewNumber] = useState('')
 
  // Read the current counter value
  const { data: currentNumber, refetch } = useReadContract({
    address: COUNTER_ADDRESS,
    abi: counterABI,
    functionName: 'number'
  })
 
  // Setup contract write hooks
  const { writeContract, data: hash } = useWriteContract()
 
  // Wait for transaction confirmation
  const { isLoading, isSuccess } = useWaitForTransactionReceipt({
    hash
  })
 
  // Update UI when transaction succeeds
  if (isSuccess) {
    refetch()
  }
 
  const handleIncrement = () => {
    writeContract({
      address: COUNTER_ADDRESS,
      abi: counterABI,
      functionName: 'increment'
    })
  }
 
  const handleSetNumber = () => {
    if (!newNumber) return
 
    writeContract({
      address: COUNTER_ADDRESS,
      abi: counterABI,
      functionName: 'setNumber',
      args: [BigInt(newNumber)]
    })
    setNewNumber('')
  }
 
  return (
    <div
      style={{
        background: 'white',
        padding: '2rem',
        borderRadius: '0.5rem',
        boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
        maxWidth: '400px',
        margin: '2rem auto'
      }}
    >
      <h2 style={{ marginBottom: '1.5rem', fontSize: '1.5rem', fontWeight: 700 }}>
        Counter Contract
      </h2>
 
      <div style={{ marginBottom: '2rem' }}>
        <div style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '0.5rem' }}>
          Current Value
        </div>
        <div style={{ fontSize: '3rem', fontWeight: 700, color: '#1f2937' }}>
          {currentNumber?.toString() ?? '—'}
        </div>
      </div>
 
      <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
        <button
          onClick={handleIncrement}
          disabled={isLoading}
          style={{
            padding: '0.75rem 1.5rem',
            borderRadius: '0.375rem',
            border: 'none',
            background: isLoading ? '#9ca3af' : '#3b82f6',
            color: 'white',
            fontWeight: 600,
            cursor: isLoading ? 'not-allowed' : 'pointer',
            fontSize: '1rem'
          }}
        >
          {isLoading ? 'Transaction pending...' : 'Increment'}
        </button>
 
        <div style={{ display: 'flex', gap: '0.5rem' }}>
          <input
            type="number"
            placeholder="Enter new value"
            value={newNumber}
            onChange={e => setNewNumber(e.target.value)}
            style={{
              flex: 1,
              padding: '0.75rem',
              borderRadius: '0.375rem',
              border: '1px solid #d1d5db',
              fontSize: '1rem'
            }}
          />
          <button
            onClick={handleSetNumber}
            disabled={isLoading || !newNumber}
            style={{
              padding: '0.75rem 1.5rem',
              borderRadius: '0.375rem',
              border: 'none',
              background: isLoading || !newNumber ? '#9ca3af' : '#10b981',
              color: 'white',
              fontWeight: 600,
              cursor: isLoading || !newNumber ? 'not-allowed' : 'pointer',
              fontSize: '1rem'
            }}
          >
            Set
          </button>
        </div>
      </div>
 
      {hash && (
        <div
          style={{
            marginTop: '1rem',
            padding: '0.75rem',
            background: '#f3f4f6',
            borderRadius: '0.375rem',
            fontSize: '0.875rem'
          }}
        >
          <div style={{ color: '#6b7280' }}>Transaction Hash:</div>
          <div
            style={{
              fontFamily: 'monospace',
              fontSize: '0.75rem',
              wordBreak: 'break-all',
              marginTop: '0.25rem'
            }}
          >
            {hash}
          </div>
        </div>
      )}
    </div>
  )
}

Integrate Counter into your App

Update your App component to include the Counter:

// src/App.tsx
import { useAccount, useConnect, useDisconnect, useSwitchChain } from 'wagmi'
import { eden } from './wagmi'
import { Counter } from './components/Counter'
 
export default function App() {
  const { address, chainId, status } = useAccount()
  const { connect, connectors, isPending: isConnecting } = useConnect()
  const { disconnect } = useDisconnect()
  const { switchChain, isPending: isSwitching } = useSwitchChain()
 
  const isOnEden = chainId === eden.id
  const metaMask = connectors.find(c => c.id === 'metaMask') ?? connectors[0]
 
  return (
    <div
      style={{
        minHeight: '100vh',
        fontFamily: 'ui-sans-serif, system-ui',
        background: '#f3f4f6',
        padding: '2rem',
        boxSizing: 'border-box'
      }}
    >
      {/* Wallet connection UI */}
      <div
        style={{
          display: 'flex',
          justifyContent: 'flex-end',
          marginBottom: '2rem'
        }}
      >
        {!address ? (
          <button
            onClick={() => connect({ connector: metaMask })}
            disabled={isConnecting}
            style={{
              padding: '0.85rem 1.5rem',
              borderRadius: '0.5rem',
              border: 'none',
              background: '#2563eb',
              color: 'white',
              fontWeight: 600,
              cursor: 'pointer',
              fontSize: '1rem'
            }}
          >
            {isConnecting ? 'Connecting…' : 'Connect MetaMask'}
          </button>
        ) : (
          <div
            style={{
              background: 'white',
              padding: '1.5rem',
              borderRadius: '0.5rem',
              boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
              display: 'flex',
              alignItems: 'center',
              gap: '1.5rem'
            }}
          >
            <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
              <span
                style={{
                  fontSize: '0.875rem',
                  color: '#6b7280',
                  cursor: 'pointer',
                  display: 'flex',
                  alignItems: 'center',
                  gap: '0.5rem'
                }}
                onClick={() => {
                  navigator.clipboard.writeText(address || '')
                  alert('Address copied!')
                }}
                title={address}
              >
                {address?.slice(0, 6)}...{address?.slice(-4)}
              </span>
              <span
                style={{
                  fontSize: '0.875rem',
                  color: '#6b7280',
                  padding: '0.25rem 0.5rem',
                  background: '#f3f4f6',
                  borderRadius: '0.25rem'
                }}
              >
                Chain: {chainId ?? '—'}
              </span>
            </div>
 
            <div style={{ display: 'flex', gap: '0.75rem' }}>
              {!isOnEden && (
                <button
                  onClick={() => switchChain({ chainId: eden.id })}
                  disabled={isSwitching}
                  style={{
                    padding: '0.5rem 1rem',
                    borderRadius: '0.375rem',
                    border: 'none',
                    background: '#f59e0b',
                    color: 'white',
                    fontWeight: 500,
                    cursor: 'pointer',
                    fontSize: '0.875rem'
                  }}
                >
                  {isSwitching ? 'Switching…' : 'Switch to Eden'}
                </button>
              )}
 
              <button
                onClick={() => disconnect()}
                style={{
                  padding: '0.5rem 1rem',
                  borderRadius: '0.375rem',
                  border: 'none',
                  background: '#dc2626',
                  color: 'white',
                  fontWeight: 500,
                  cursor: 'pointer',
                  fontSize: '0.875rem'
                }}
              >
                Disconnect
              </button>
            </div>
          </div>
        )}
      </div>
 
      {/* Show Counter when connected to Eden */}
      {address && isOnEden && <Counter />}
 
      {/* Show message when connected but on wrong network */}
      {address && !isOnEden && (
        <div
          style={{
            textAlign: 'center',
            marginTop: '4rem',
            fontSize: '1.125rem',
            color: '#6b7280'
          }}
        >
          Please switch to Eden to interact with the contract.
        </div>
      )}
    </div>
  )
}

Run your app

npm run dev

Visit http://localhost:5173 and:

  1. Connect your MetaMask wallet
  2. Switch to Eden if needed
  3. Interact with your Counter contract

Branding

Eden provides official logos for use in your projects:

  • Eden icon (SVG | PNG) - Use this for representing the Eden chain
  • TIA icon (SVG | PNG) - Use this for representing the TIA token

Download the logos from the GitHub repository.

Next steps

  • Add error handling and loading states
  • Display transaction confirmations
  • Add more complex contract interactions
  • Style with your preferred CSS framework