#
Web3
QuickDapp provides comprehensive Web3 integration through RainbowKit, Wagmi, and Viem. This enables seamless wallet connections, blockchain interactions, and smart contract management with excellent TypeScript support.
#
Wallet Configuration
The wallet configuration is set up in the root of your application:
// src/client/lib/wagmi.ts
import { createConfig, http } from 'wagmi'
import { sepolia, anvil } from 'wagmi/chains'
import { connectorsForWallets } from '@rainbow-me/rainbowkit'
import {
metaMaskWallet,
walletConnectWallet,
coinbaseWallet,
rainbowWallet
} from '@rainbow-me/rainbowkit/wallets'
import { clientConfig } from '@shared/config/client'
// Define available wallets
const connectors = connectorsForWallets(
[
{
groupName: 'Recommended',
wallets: [metaMaskWallet, rainbowWallet, coinbaseWallet],
},
{
groupName: 'Others',
wallets: [walletConnectWallet],
},
],
{
appName: 'QuickDapp',
projectId: clientConfig.WALLETCONNECT_PROJECT_ID,
}
)
// Wagmi configuration
export const wagmiConfig = createConfig({
connectors,
chains: [
clientConfig.CHAIN === 'sepolia' ? sepolia : anvil,
],
transports: {
[sepolia.id]: http(clientConfig.CHAIN_RPC_ENDPOINT),
[anvil.id]: http(clientConfig.CHAIN_RPC_ENDPOINT),
},
})
#
Provider Setup
Wrap your application with the necessary providers:
// src/client/main.tsx
import { WagmiProvider } from 'wagmi'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { wagmiConfig } from './lib/wagmi'
import '@rainbow-me/rainbowkit/styles.css'
const queryClient = new QueryClient()
export function App() {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/tokens" element={<TokensPage />} />
</Routes>
</Router>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
)
}
#
Wallet Connection
#
Basic Connection Hook
// src/client/hooks/useWalletConnection.ts
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { useEffect } from 'react'
import { useAuth } from './useAuth'
export function useWalletConnection() {
const { address, isConnected, isConnecting } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
const { signIn, signOut, user } = useAuth()
// Auto-authenticate when wallet connects
useEffect(() => {
if (isConnected && address && !user) {
signIn(address)
} else if (!isConnected && user) {
signOut()
}
}, [isConnected, address, user, signIn, signOut])
return {
address,
isConnected,
isConnecting,
connect,
disconnect,
connectors
}
}
#
Connect Button Component
// src/client/components/ConnectButton.tsx
import { ConnectButton as RainbowConnectButton } from '@rainbow-me/rainbowkit'
import { useAuth } from '../hooks/useAuth'
import { Button } from './ui/Button'
export function ConnectButton() {
return (
<RainbowConnectButton.Custom>
{({
account,
chain,
openAccountModal,
openChainModal,
openConnectModal,
authenticationStatus,
mounted,
}) => {
const ready = mounted && authenticationStatus !== 'loading'
const connected =
ready &&
account &&
chain &&
(!authenticationStatus || authenticationStatus === 'authenticated')
return (
<div
{...(!ready && {
'aria-hidden': true,
style: {
opacity: 0,
pointerEvents: 'none',
userSelect: 'none',
},
})}
>
{(() => {
if (!connected) {
return (
<Button onClick={openConnectModal}>
Connect Wallet
</Button>
)
}
if (chain.unsupported) {
return (
<Button onClick={openChainModal} variant="destructive">
Wrong network
</Button>
)
}
return (
<div className="flex gap-2">
<Button
onClick={openChainModal}
variant="outline"
size="sm"
>
{chain.hasIcon && (
<div className="w-3 h-3 mr-1">
{chain.iconUrl && (
<img
alt={chain.name ?? 'Chain icon'}
src={chain.iconUrl}
/>
)}
</div>
)}
{chain.name}
</Button>
<Button
onClick={openAccountModal}
size="sm"
>
{account.displayName}
</Button>
</div>
)
})()}
</div>
)
}}
</RainbowConnectButton.Custom>
)
}
#
Smart Contract Interactions
#
Reading Contract Data
// src/client/hooks/useTokenBalance.ts
import { useReadContract } from 'wagmi'
import { ERC20_ABI } from '../lib/abis'
import { useAccount } from 'wagmi'
export function useTokenBalance(tokenAddress: string) {
const { address } = useAccount()
const { data: balance, isLoading, error } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
query: {
enabled: !!address && !!tokenAddress,
refetchInterval: 10000, // Refetch every 10 seconds
}
})
return {
balance: balance || BigInt(0),
isLoading,
error
}
}
export function useTokenInfo(tokenAddress: string) {
const { data: name } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'name',
})
const { data: symbol } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'symbol',
})
const { data: decimals } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'decimals',
})
const { data: totalSupply } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'totalSupply',
})
return {
name: name as string,
symbol: symbol as string,
decimals: decimals as number,
totalSupply: totalSupply as bigint,
}
}
#
Writing to Contracts
// src/client/hooks/useTokenTransactions.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { ERC20_ABI } from '../lib/abis'
import { toast } from 'react-hot-toast'
export function useTokenTransfer(tokenAddress: string, decimals: number = 18) {
const {
writeContract,
data: hash,
isPending,
error: writeError
} = useWriteContract()
const {
isLoading: isConfirming,
isSuccess,
error: receiptError
} = useWaitForTransactionReceipt({
hash,
})
const transfer = async (to: string, amount: string) => {
try {
const amountBigInt = parseUnits(amount, decimals)
writeContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'transfer',
args: [to as `0x${string}`, amountBigInt],
})
toast.success('Transaction submitted!')
} catch (error) {
toast.error('Failed to submit transaction')
console.error(error)
}
}
// Handle transaction confirmation
React.useEffect(() => {
if (isSuccess) {
toast.success('Transfer completed!')
} else if (receiptError) {
toast.error('Transaction failed')
}
}, [isSuccess, receiptError])
return {
transfer,
isPending,
isConfirming,
isSuccess,
hash,
error: writeError || receiptError
}
}
export function useTokenApproval(tokenAddress: string, decimals: number = 18) {
const { writeContract, data: hash, isPending } = useWriteContract()
const approve = async (spender: string, amount: string) => {
const amountBigInt = parseUnits(amount, decimals)
writeContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'approve',
args: [spender as `0x${string}`, amountBigInt],
})
}
return { approve, hash, isPending }
}
#
Factory Contract Interactions
// src/client/hooks/useTokenFactory.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { useReadContract } from 'wagmi'
import { FACTORY_ABI } from '../lib/abis'
import { clientConfig } from '@shared/config/client'
import { parseEther } from 'viem'
export function useDeployToken() {
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
})
const deployToken = async (name: string, symbol: string, initialSupply: string) => {
writeContract({
address: clientConfig.FACTORY_CONTRACT_ADDRESS as `0x${string}`,
abi: FACTORY_ABI,
functionName: 'deployToken',
args: [name, symbol, parseEther(initialSupply)],
})
}
return {
deployToken,
isPending,
isConfirming,
isSuccess,
hash
}
}
export function useUserTokens() {
const { address } = useAccount()
const { data: tokenCount } = useReadContract({
address: clientConfig.FACTORY_CONTRACT_ADDRESS as `0x${string}`,
abi: FACTORY_ABI,
functionName: 'getUserTokenCount',
args: address ? [address] : undefined,
query: {
enabled: !!address,
}
})
const { data: tokens } = useReadContract({
address: clientConfig.FACTORY_CONTRACT_ADDRESS as `0x${string}`,
abi: FACTORY_ABI,
functionName: 'getUserTokens',
args: address ? [address, 0n, tokenCount || 0n] : undefined,
query: {
enabled: !!address && !!tokenCount,
}
})
return {
tokens: (tokens as string[]) || [],
tokenCount: tokenCount as bigint
}
}
#
Utility Functions
#
Format and Parse Values
// src/client/lib/web3-utils.ts
import { formatUnits, parseUnits, isAddress } from 'viem'
export function formatTokenAmount(
value: bigint,
decimals: number = 18,
displayDecimals: number = 4
): string {
const formatted = formatUnits(value, decimals)
return parseFloat(formatted).toFixed(displayDecimals)
}
export function parseTokenAmount(value: string, decimals: number = 18): bigint {
try {
return parseUnits(value, decimals)
} catch {
return BigInt(0)
}
}
export function shortenAddress(address: string, chars: number = 4): string {
if (!isAddress(address)) return address
return `${address.slice(0, 2 + chars)}...${address.slice(-chars)}`
}
export function isValidAddress(address: string): boolean {
return isAddress(address)
}
#
Error Handling
// src/client/lib/web3-errors.ts
export function parseContractError(error: Error): string {
const message = error.message
// User rejected transaction
if (message.includes('User rejected') || message.includes('rejected')) {
return 'Transaction was rejected'
}
// Insufficient funds
if (message.includes('insufficient funds')) {
return 'Insufficient funds for transaction'
}
// Gas estimation failed
if (message.includes('gas')) {
return 'Transaction may fail - check gas settings'
}
// Contract revert
if (message.includes('revert')) {
// Try to extract revert reason
const revertMatch = message.match(/revert (.+)/)
if (revertMatch) {
return revertMatch[1]
}
return 'Transaction would revert'
}
return 'Transaction failed'
}
#
Component Examples
#
Token Transfer Form
// src/client/components/forms/TransferTokenForm.tsx
import { useState } from 'react'
import { useTokenTransfer, useTokenInfo } from '../../hooks/useTokenTransactions'
import { parseTokenAmount, formatTokenAmount, isValidAddress } from '../../lib/web3-utils'
import { Button } from '../ui/Button'
import { Input } from '../ui/Input'
interface TransferTokenFormProps {
tokenAddress: string
onSuccess?: () => void
}
export function TransferTokenForm({ tokenAddress, onSuccess }: TransferTokenFormProps) {
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
const { symbol, decimals } = useTokenInfo(tokenAddress)
const { transfer, isPending, isConfirming } = useTokenTransfer(tokenAddress, decimals)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!isValidAddress(recipient)) {
alert('Invalid recipient address')
return
}
if (parseFloat(amount) <= 0) {
alert('Amount must be greater than 0')
return
}
try {
await transfer(recipient, amount)
onSuccess?.()
} catch (error) {
console.error('Transfer failed:', error)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Input
label="Recipient Address"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
required
/>
</div>
<div>
<Input
label={`Amount (${symbol})`}
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
step="any"
min="0"
required
/>
</div>
<Button
type="submit"
disabled={isPending || isConfirming}
loading={isPending || isConfirming}
>
{isPending ? 'Confirming...' : isConfirming ? 'Processing...' : 'Transfer'}
</Button>
</form>
)
}
#
Network Switching
// src/client/components/NetworkSwitcher.tsx
import { useSwitchChain, useChainId } from 'wagmi'
import { sepolia, anvil } from 'wagmi/chains'
const SUPPORTED_CHAINS = [sepolia, anvil]
export function NetworkSwitcher() {
const chainId = useChainId()
const { switchChain, isPending } = useSwitchChain()
const currentChain = SUPPORTED_CHAINS.find(chain => chain.id === chainId)
return (
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Network:</span>
<select
value={chainId}
onChange={(e) => switchChain({ chainId: parseInt(e.target.value) })}
disabled={isPending}
className="px-3 py-1 text-sm border rounded-md"
>
{SUPPORTED_CHAINS.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.name}
</option>
))}
</select>
{isPending && (
<span className="text-xs text-gray-500">Switching...</span>
)}
</div>
)
}
The Web3 integration in QuickDapp provides a complete toolkit for building modern dApps with excellent user experience and robust error handling.