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 constConfigure 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>
)
}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